#include <cassert>
#include <cctype>

#include "fmod.h"
#include "irrlicht.h"

#include "demo.h"
#include "Turs2.h"


#include "fx_starttexts.h"
#include "fx_logopts.h"
#include "fx_aivop.h"
#include "fx_kuksa.h"
#include "fx_closer.h"
#include "fx_car.h"
#include "fx_hallintaa.h"
#include "fx_hall.h"
#include "fx_holo.h"
#include "fx_kimble.h"
#include "fx_kuja.h"
#include "fx_lampskutta.h"
#include "fx_alyvuoto.h"
#include "fx_puffpts.h"
#include "fx_endtexts.h"


struct FxDesc
{
    const char *name;
    Fx *fx;
    // Start & end time of fx rendering in seconds
    double renderStartTime;
    double renderEndTime;
    // Start & end effect "main period" reference times in seconds, initialized to fx object
    double fxStartTime;
    double fxEndTime;
};

static StartTextsFx fx_starttexts;
static LogoPtsFx fx_logopts;
static AivopFx fx_aivop;
static KuksaFx fx_kuksa;
static CloserFx fx_closer;
static CarFx fx_car;
static HallintaaFx fx_hallintaa;
//static HoloFx fx_holo;
static KimbleFx fx_kimble;
static KujaFx fx_kuja;
//static LampskuttaFx fx_lampskutta;
//static HallFx fx_hall;
static AlyvuotoFx fx_alyvuoto;
static PuffPtsFx fx_puffpts;
static EndTextsFx fx_endtexts;

#define START_TIME_OFFSET   0

#define DEMO_LENGTH         195.000

#define FX(name) #name,&name
// Each frame: all fx objects get rendered in this order if the demo time is between render start & end time for the fx.
static FxDesc sFxDescs[] =
{
    //            render start,end      ref start,end

    { FX(fx_starttexts), 0.000,  15.330,   0.000,  14.330 }, // start texts (1992 ... etc.)

    { FX(fx_logopts),   14.330,  33.994,  15.330,  29.738 },

    { FX(fx_hallintaa), 29.738,  53.000,  29.738,  50.992 }, //valmis

    { FX(fx_kuja),      50.992,  67.947,  50.992,  67.947 }, //valmis

    { FX(fx_kimble),    67.947,  84.958,  67.947,  84.958 }, //valmis
    
    { FX(fx_car),       84.958, 101.955,  84.958, 101.955 }, //prelivalmis

    { FX(fx_aivop),    101.955, 110.440, 101.955, 110.440 },

    { FX(fx_alyvuoto), 110.440, 135.922, 110.440, 135.922 },

    { FX(fx_closer),   135.922, 152.933, 135.922, 152.933 }, //valmis

    { FX(fx_endtexts), 152.933, 195.000, 152.933, 195.000 },

//  //{ FX(fx_kuksa),      7.000,   8.000,   7.000,   9.000 },
//  //{ FX(fx_lampskutta),  0.000,  40.000,   0.000,  40.000 }, // almost useless
//  //{ FX(fx_holo),  0.000,  40.000,   0.000,  40.000 },

//  //{ FX(fx_hall),  0.000,  40.000,   0.000,  40.000 },
    

    { FX(fx_puffpts), 0, 1000000.0, 0, 1000000.0 }, // always rendered & hack in render_fx
};
#define FX_COUNT (sizeof(sFxDescs) / sizeof(sFxDescs[0]))


using namespace irr;

int gWidth = DEFAULT_WIDTH, gHeight = DEFAULT_HEIGHT;
bool gAlive = true;
double gTime = 0;
int gTimeMs = 0;
IrrlichtDevice *gIrrDevice = 0;
irrklang::ISoundEngine *gSoundEngine = 0;
int gSeek = 0;

static FSOUND_STREAM *sMusic;
static int sMusicChannel = -1;

turska::Graphics *gT2Graphics = 0;
extern turska::Font *gT2FontBig = 0;


static bool sMusicStopped = false;

static signed char F_CALLBACKAPI sMusicCallback(FSOUND_STREAM *stream, void *buff, int len, void *userdata)
{
    sMusicStopped = true;
    return 0;
}


class EventReceiver : public IEventReceiver
{
    virtual bool OnEvent(const SEvent &event)
    {
        if (event.EventType == EET_KEY_INPUT_EVENT && event.KeyInput.PressedDown && event.KeyInput.Key == KEY_ESCAPE)
        {
            gAlive = false; // ESC kills the demo
            return true;
        }

#ifdef _DEBUG
        if (event.EventType == EET_KEY_INPUT_EVENT && event.KeyInput.PressedDown && event.KeyInput.Key == KEY_LEFT)
        {
            --gSeek;
        }
        if (event.EventType == EET_KEY_INPUT_EVENT && event.KeyInput.PressedDown && event.KeyInput.Key == KEY_RIGHT)
        {
            ++gSeek;
        }
#endif

        return false;
    }
};

static EventReceiver sEventReceiver;


static bool init_window(const char *cmd)
{
    bool fullscreen = true;

    // force windowed?
    if (cmd && strlen(cmd) > 0 && toupper(cmd[0]) == 'W')
        fullscreen = false;
#ifdef _DEBUG
    fullscreen = false;
#endif

    SIrrlichtCreationParameters icp;
    // changes to defaults:
    icp.DriverType = video::EDT_OPENGL;
    icp.WindowSize.set((u32)gWidth, (u32)gHeight);
    icp.Bits = 32;
    icp.ZBufferBits = 24;
    icp.Fullscreen = fullscreen;
    icp.Stencilbuffer = true;
    icp.Vsync = true;
    icp.AntiAlias = 16;
    icp.HandleSRGB = false; // do we want this or not?
    icp.EventReceiver = &sEventReceiver;
    icp.UsePerformanceTimer = false;

    gIrrDevice = createDeviceEx(icp);

    if (gIrrDevice == 0)
        return false;

    if (gIrrDevice)
    {
        // fill screen for further loading/init
        video::IVideoDriver *videoDriver = gIrrDevice->getVideoDriver();
        videoDriver->beginScene(true, true, video::SColor(0xff223344));
        videoDriver->endScene();
    }

    #ifndef _DEBUG
    gIrrDevice->getCursorControl()->setVisible(false);
    #endif

    gIrrDevice->getFileSystem()->addFileArchive("data/oldies.zip", false, false, io::EFAT_ZIP);

    return true;
}

static void deinit_window()
{
    if (!gIrrDevice)
        return;
    gIrrDevice->drop();
    gIrrDevice = 0;
}


static const int AUDIO_CHANNELS = 8;

static bool init_sound()
{
    if (!FSOUND_Init(44100, AUDIO_CHANNELS, 0))
    {
        gErrorMessage("Audio error", "Unable to initialize sound! (%d)",
                      FSOUND_GetError());
    }
    sMusic = FSOUND_Stream_Open("data/music.ogg", FSOUND_MPEGACCURATE, 0, 0);
    if (!sMusic)
        gErrorMessage("Audio error", "Can't load music");
    if (sMusic)
        FSOUND_Stream_SetEndCallback(sMusic, sMusicCallback, 0);
    return true;
}

static void start_sound()
{
    sMusicChannel = FSOUND_Stream_Play(FSOUND_FREE, sMusic);
}

static void deinit_sound()
{
    if (sMusic)
    {
        FSOUND_Stream_Stop(sMusic);
        FSOUND_Stream_Close(sMusic);
        sMusic = 0;
    }
    for (int a = 0; a < AUDIO_CHANNELS; ++a)
        FSOUND_StopSound(a);
    FSOUND_Close();
}


void init_fx()
{
    video::IVideoDriver *videoDriver = gIrrDevice->getVideoDriver();
    videoDriver->beginScene(true, true, video::SColor(0xff223355));

    for (int a = 0; a < FX_COUNT; ++a)
    {
        FxDesc &desc = sFxDescs[a];
        assert(desc.fx);
        desc.fx->setFxTimes(desc.fxStartTime, desc.fxEndTime);
        desc.fx->setRenderTimes(desc.renderStartTime, desc.renderEndTime);
        bool initResult = desc.fx->init();
        if (!initResult)
        {
            gErrorMessage("Error", "Can't init Fx: %s", desc.name);
            desc.fx = 0;
        }
        desc.fx->render(-1, -1);  // render 1 frame with each effect in init phase
    }

    videoDriver->draw2DRectangle(irr::core::recti(0, 0, gWidth, gHeight), video::SColor(0xff000000), video::SColor(0xff000000), video::SColor(0xff000000), video::SColor(0xff000000));
	videoDriver->endScene();
}

void deinit_fx()
{
    for (int a = 0; a < FX_COUNT; ++a)
    {
        FxDesc &desc = sFxDescs[a];
        if (!desc.fx)
            continue;
        desc.fx->deinit();
        desc.fx = 0;
    }
}

void render_fx()
{
    //fx_puffpts.mColor;

    // puffpts effect fade hack:
    // figure out min. distance of current time to any start/end render point of all effects
    float minDist = 1000000.0f;
    for (int a = 0; a < FX_COUNT; ++a)
    {
        FxDesc &desc = sFxDescs[a];
        float dist1 = (float)fabs(gTime - desc.renderStartTime);
        float dist2 = (float)fabs(gTime - desc.renderEndTime);
        minDist = core::min_(minDist, dist1, dist2);
    }
    // set puff color by the distance
    {
        const float peakSeconds = 2.5f;
        float normalizedDist = 1.0f - core::clamp<float>(minDist / peakSeconds, 0, 1);
        float puffAlpha = normalizedDist * 0.98f + 0.02f;
        fx_puffpts.mColor.set(puffAlpha, 1, 1, 1);
    }

    for (int a = 0; a < FX_COUNT; ++a)
    {
        FxDesc &desc = sFxDescs[a];
        if (!desc.fx)
            continue;
        if (gTime >= desc.renderStartTime && gTime< desc.renderEndTime)
            desc.fx->render(gTime, gTimeMs);
    }
}


bool demo_main(const char *cmd)
{
    bool initSuccess = init_window(cmd);
    if (!initSuccess)
    {
        gErrorMessage("Error", "Error while initializing window");
        return false;
    }
    initSuccess = init_sound();
    if (!initSuccess)
    {
        gErrorMessage("Error", "Error while initializing sound");
        deinit_window();
        return false;
    }
    if (!initSuccess)
        return false;

    gT2Graphics = new turska::GraphicsIrr(gIrrDevice, 4096);
    assert(gT2Graphics);
    gT2FontBig = new turska::BMFont(gT2Graphics, "data/orfk110.fnt");
    assert(gT2FontBig);
    bool fontInitResult = gT2FontBig->init();
    assert(fontInitResult);

    init_fx();

    start_sound();

    int frameCount = 0;

    video::IVideoDriver *videoDriver = gIrrDevice->getVideoDriver();
    //scene::ISceneManager *sceneManager = gIrrDevice->getSceneManager();

    int startTime = 0;
    int timeOffset = (int)(START_TIME_OFFSET * 1000);

    if (timeOffset > 0)
    {
        FSOUND_Stream_SetTime(sMusic, timeOffset);
    }

    while (gIrrDevice && gIrrDevice->run() && gAlive)
	{
        if (gTime >= DEMO_LENGTH)
            gAlive = false;

        bool resetMusicPos = false;
        if (gSeek != 0)
        {
            timeOffset += gSeek * 1000;
            gSeek = 0;
            resetMusicPos = true;
        }
        if (startTime == 0)
            startTime = (int)gIrrDevice->getTimer()->getTime();
        gTimeMs = (int)gIrrDevice->getTimer()->getTime() - startTime + timeOffset;
        if (gTimeMs < 0)
        {
            timeOffset += -gTimeMs;
            gTimeMs = 0;
        }
        if (resetMusicPos)
            FSOUND_Stream_SetTime(sMusic, gTimeMs);

        gTime = gTimeMs / 1000.0;

        videoDriver->beginScene(true, true, video::SColor(0xff223355));

        render_fx();

		videoDriver->endScene();

        if (++frameCount % 100 == 0)
        {
			core::stringw str = L"Demo - fps: ";
			str += (s32)videoDriver->getFPS();
			gIrrDevice->setWindowCaption(str.c_str());
        }

		gIrrDevice->sleep(10);
	}

    delete gT2FontBig;
    delete gT2Graphics;

    deinit_fx();
    deinit_sound();
    deinit_window();

    return true;
}
